﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections.ObjectModel;

using P2PLib.Common;
using P2PLib.Common.DataModel;
using P2PLib.Common.Command;
using P2PLib.Client.Command;
using P2PLib.Client.ViewModel;
using P2PLib.Client.DataModel;
using P2PLib.Client.Utils;

namespace P2PLib.Client
{
    public class Client:INotifyPropertyChanged
    {
        #region filed;
        //static filed
        //if connect to server takes more than 3000 then connection failed
        public static int ConnectionTimeOut = 3000;

        //server inforamtion;
        private IPEndPoint serverEP;

        //client fileds
        private Socket clientSocket;
        private NetworkStream netStream;
        private BackgroundWorker bgReciever;
        private ClientInfo info;
        private ClientServer cServer;
        private List<ClientInfo> onlineClients;
        private ObservableCollection<SearchResultViewModel> searchResults;

        private ObservableCollection<DownloadTaskViewModel> tasks;
        private ObservableCollection<LocalFileInfo> files;
        #endregion

        #region Property
        public IPAddress ServerIP
        {
            get { return ((IPEndPoint)serverEP).Address; }
        }

        public int Port
        {
            get { return ((IPEndPoint)serverEP).Port; }
        }

        public bool IsConnected
        {
            get 
            {
                if (this.clientSocket != null)
                    return this.clientSocket.Connected;
                else
                    return false;
            }
        }

        public ClientInfo Info
        {
            get { return info; }
        }

        public ObservableCollection<SearchResultViewModel> SearchResults
        {
            get
            {
                return this.searchResults;
            }
            set
            {
                this.searchResults = value;
            }
        }

        public ObservableCollection<DownloadTaskViewModel> Tasks
        {
            get
            {
                return this.tasks;
            }
            set
            {
                this.tasks = value;
            }
        }

        public ObservableCollection<LocalFileInfo> Files
        {
            get
            {
                return files;
            }
            set
            {
                this.files = value;
            }
        }
        #endregion

        #region Constructor

        public Client(IPEndPoint serverEP,ClientInfo info)
        {
            this.serverEP = serverEP;
            this.info = info;
            //isConnected=false;
            this.onlineClients = new List<ClientInfo>();
            this.searchResults = new ObservableCollection<SearchResultViewModel>();
        }

        public Client(string serverIP, int port,ClientInfo info)
        {
            this.serverEP = new IPEndPoint(IPAddress.Parse(serverIP), port);

            this.info = info;
            //this.isConnected = false;
            this.onlineClients = new List<ClientInfo>();
            this.searchResults = new ObservableCollection<SearchResultViewModel>();

        }
        #endregion

        #region methods

        /// <summary>
        /// start a bcakground worker and try to connect to 
        /// the server
        /// </summary>
        public void ConnectToServer()
        {
            //start a background worker to connect to the server;
            BackgroundWorker bgConnector = new BackgroundWorker();
            bgConnector.DoWork+=new DoWorkEventHandler(bgConnector_DoWork);
            bgConnector.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgConnector_RunWorkerCompleted);
            bgConnector.RunWorkerAsync();
        }

        /// <summary>
        /// occurs after bgConnector has finished its work
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgConnector_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (!(bool)e.Result)
            {
                //connection failed;
                this.OnConnectingServerFailed();
            }
            else
            {
                //successfully connected to the server;
                this.OnConnectingServerSucceed();
            }
        }

        /// <summary>
        /// connect to the server, if succeed start a background worker to revieve data from 
        /// server, else set e.Result to false;
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgConnector_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
                    ProtocolType.Tcp);
                this.clientSocket.Connect(serverEP);

                
                this.netStream = new NetworkStream(this.clientSocket);
                //send client info to server
                this.SendCommandToServer(new CSConnectCommand(this.info));

                //wait for server to send clientinfo
                while (true)
                {
                    byte[] buffer = new byte[4];
                    netStream.Read(buffer, 0, 4);
                    CSCommandType cmdType = (CSCommandType)BitConverter.ToInt32(buffer, 0);

                    if(cmdType.Equals(CSCommandType.ClientInfo))
                    {
                        CSClientInfoCommand cic=new CSClientInfoCommand();
                        cic.ReadFromStream(netStream);

                        foreach (ClientInfo ci in cic.Clients)
                        {
                            this.onlineClients.Add(ci);
                        }
                        break;
                    }
                }
                e.Result = true;
                //create the instace of bgReciever
                this.bgReciever = new BackgroundWorker();
                bgReciever.DoWork += new DoWorkEventHandler(StartRecieve);
                bgReciever.WorkerSupportsCancellation = true;
                bgReciever.RunWorkerAsync();
            }
            catch
            {
                e.Result = false;
            }
        }

        /// <summary>
        /// 发送命令到分服务器端;
        /// </summary>
        /// <param name="command"></param>
        public void SendCommandToServer(CSCommandBase command)
        {
            BackgroundWorker bgSender = new BackgroundWorker();
            bgSender.DoWork += new DoWorkEventHandler(bgSender_DoWork);
            bgSender.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgSender_RunWorkerCompleted);
            bgSender.WorkerSupportsCancellation = true;
            bgSender.RunWorkerAsync(command);
        }

        /// <summary>
        /// send a command to server
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void bgSender_DoWork(object sender, DoWorkEventArgs e)
        {
            CSCommandBase cmd = (CSCommandBase)e.Argument;
            if (this.clientSocket != null && this.clientSocket.Connected)
            {
                //send the command to server;
                byte[] buffer = new byte[4];
                buffer = BitConverter.GetBytes((int)cmd.Type);

                netStream.Write(buffer, 0, 4);
                netStream.Flush();
                cmd.WriteToStream(netStream);

                netStream.Flush();
                //set e.Result to true if send succeed else set e.Result=false;
                e.Result = true;
            }
            else
            {
                Console.WriteLine("is clientSocket null?{0}", this.clientSocket == null);

                e.Result = false;
            }
        }

        /// <summary>
        /// check the result 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void bgSender_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if ((bool)e.Result)
            {
                //send command succeed
                Debug.WriteLine("Send command succeed...");
            }
            else
            {
                //failed
                Debug.WriteLine("Send command failed...");
                //re-sent the command
                
            }
        }

        /// <summary>
        /// start recieve command from server
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void StartRecieve(object sender, DoWorkEventArgs e)
        {
            //start recieving command from server;
            Debug.WriteLine("Started recieving  command from server...");
           
            //define how to recive command
            try
            {
                while (this.clientSocket.Connected)
                {
                    //读取命令的类型;
                    byte[] buffer = new byte[4];
                    int readBytes = netStream.Read(buffer, 0, 4);
                    if (readBytes != 0)
                    {
                        CSCommandType type = (CSCommandType)BitConverter.ToInt32(buffer, 0);

                        Debug.WriteLine("Recieved new command ,type :" + type);


                        switch (type)
                        {
                            case CSCommandType.CheckOnline:
                                //回复服务器的检查;
                                CSResponseCheckOnlineCommand rcc = new CSResponseCheckOnlineCommand();
                                this.SendCommandToServer(rcc);
                                break;
                            case CSCommandType.ClientDisconnect:
                                CSClientDisconnectCommand cdc = new CSClientDisconnectCommand();
                                cdc.ReadFromStream(netStream);
                                //从当前的已知的客户端列表中移除
                                try
                                {
                                    this.RemoveClient(cdc.Client);
                                }
                                catch (Exception ex)
                                {
                                    Debug.WriteLine("client: exception occured when removing client");
                                }
                                break;
                            case CSCommandType.ClientInfo:
                                CSClientInfoCommand cic = new CSClientInfoCommand();
                                cic.ReadFromStream(netStream);
                                //设置自己当前已经知道的客户端列表；
                                foreach (ClientInfo ci in cic.Clients)
                                    this.onlineClients.Add(ci);
                                //this.onlineClients = cic.Clients;
                                Debug.WriteLine("Current online clients:" + this.onlineClients.Count);
                                break;
                            case CSCommandType.NewClient:
                                CSNewClientCommand ncc = new CSNewClientCommand();
                                ncc.ReadFromStream(netStream);

                                //add a new client to current client list
                                Debug.WriteLine("New Client enter the network:"+ncc.Client.IP.ToString());

                                if (onlineClients == null)
                                {
                                    this.onlineClients = new List<ClientInfo>();
                                    this.onlineClients.Add(ncc.Client);
                                }
                                else
                                {
                                    int index = onlineClients.IndexOf(ncc.Client);
                                    if (index == -1)
                                        this.onlineClients.Add(ncc.Client);
                                }
                                break;
                            default:
                                //something went wrong
                                break;
                        }
                    }
                }

                Debug.WriteLine("Client ,clientSocket disconnected...");
            }
            catch (Exception ee)
            {
                Debug.WriteLine(ee.StackTrace);
            }
        }

        /// <summary>
        /// 搜索P2P网络，按照关键字的
        /// </summary>
        /// <param name="keyword"></param>
        public void SearchFileByName(string keyword)
        {
            //clear previous search result;
            this.searchResults.Clear();

            this.StartSearch(SearchType.ByKeyWord, keyword);
        }

        /// <summary>
        /// search file by hashcode
        /// </summary>
        /// <param name="id"></param>
        public void SearchFileById(string id)
        {
            this.StartSearch(SearchType.ByHashCode, id);
        }

        /// <summary>
        /// 为每一个已知的客户端创建一个线程
        /// 连接到该客户端之后会向该客户端发送搜索消息，
        /// 然后等待客户端返回结果
        /// </summary>
        /// <param name="type"></param>
        /// <param name="key"></param>
        private  void StartSearch(SearchType type,string key)
        {
            Debug.WriteLine("start searching from onlineclients ,count:" + onlineClients.Count);

            foreach (ClientInfo c in onlineClients)
            {
                if(c.IP.ToString().Equals(this.info.IP.ToString()))
                    continue;

                //start a backgournd worker to connect to the client and 
                //send the search command to the client;
                BackgroundWorker bgSearcher = new BackgroundWorker();
                bgSearcher.DoWork += new DoWorkEventHandler(bgSearcher_DoWork);
                bgSearcher.RunWorkerAsync(new object[]{c,type,key});
            }
        }


        /// <summary>
        /// send the search command to client and wait for 
        /// response;
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void bgSearcher_DoWork(object sender, DoWorkEventArgs e)
        {
           //the client to connect
            object[] args=(object[])e.Argument;
            ClientInfo c = (ClientInfo)args[0];
            SearchType type=(SearchType)args[1];
            string key=(string)args[2];

            Debug.WriteLine("Searching client "+c.IP.ToString());
            List<FileResult> results = new List<FileResult>();
            
            try
            {
                //连接到客户端;
                Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.Connect(new IPEndPoint(c.IP,c.Port));
                NetworkStream cStream = new NetworkStream(socket);
                CCSearchCommand csc = new CCSearchCommand(type, key);
                //发送搜索消息到客户端;
                byte[] buffer = BitConverter.GetBytes((int)csc.Type);
                cStream.Write(buffer, 0, 4);
                csc.WriteToStream(cStream);

                Debug.WriteLine("clinent:sending command " + csc.Type);

                cStream.Flush();

                //wait for client to return search result;
                //set the readtimeout to 3 seconds
                cStream.ReadTimeout = 3000;

                int readBytes= cStream.Read(buffer, 0, 4);
                if (readBytes == 0)
                {
                    Debug.WriteLine("client " + c.IP + " didn't responsed to search command");
                }
                else
                {
                    //expected command type is Result
                    CCCommandType cmdType = (CCCommandType)BitConverter.ToInt32(buffer,0);
                    if (!cmdType.Equals(CCCommandType.Result))
                    {
                        Debug.WriteLine("unexpected command from " + c.IP.ToString());
                    }
                    else
                    {
                        CCResultCommand crc = new CCResultCommand();
                        crc.ReadFromStream(cStream);

                        Debug.WriteLine("recived result from " + c.IP.ToString());
                        //process the result
                        results.Add(new FileResult(crc.Files,c));
                    }
                }
            }
            catch(Exception ex)
            {
                Debug.WriteLine("search client:"+c.IP.ToString()+" failed..");
                Debug.WriteLine("message:" + ex.Message + Environment.NewLine + "stacktrace:" + ex.StackTrace);
            }

            if (type.Equals(SearchType.ByKeyWord))
            {
                this.UpdateSearchResult(results);
            }
            else
            {

            }

        }

        /// <summary>
        /// DownloadTaskViewModel按照文件Id搜索文件，得到返回结果
        /// 后将结果添加到soure
        /// </summary>
        /// <param name="crc"></param>
        private void AddResultToDownloadTask(CCResultCommand crc,ClientInfo client)
        {
            //将搜索到的结果添加到任务的源中：
            Debug.WriteLine("Adding result to task..");
        }

        /// <summary>
        /// reset the result list 
        /// </summary>
        /// <param name="results"></param>
        private void UpdateSearchResult(List<FileResult> results)
        {
            ObservableCollection<SearchResultViewModel> sResult = new ObservableCollection<SearchResultViewModel>();

            foreach (FileResult fr in results)
            {
                List<CFileInfo> files = fr.Files;
                ClientInfo clientInfo = fr.Client;

                foreach (CFileInfo cf in files)
                {
                    //check if this file has alreday been added to sResult
                    var temp = from s in sResult
                               where s.File.FileHashCode.Equals(cf.FileHashCode)
                               select s;
                    if (temp.Count() != 0)
                    {
                        foreach (SearchResultViewModel srvm in temp)
                        {
                            srvm.Source.Add(clientInfo);
                        }
                    }
                    else
                    {
                        sResult.Add(new SearchResultViewModel(cf,clientInfo));
                    }
                }
                this.searchResults=sResult;
                
                if(this.PropertyChanged!=null)
                {
                    this.PropertyChanged(this,new PropertyChangedEventArgs("SearchResults"));
                }
            }
        }

        /// <summary>
        /// remove a client from online clients list
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        private void RemoveClient(ClientInfo c)
        {
            
            lock (this)
            {
                int index=-1;
                foreach (ClientInfo ci in onlineClients)
                {
                    if (c.IP.ToString().Equals(ci.IP.ToString()))
                    {
                        index = onlineClients.IndexOf(ci);
                        break;
                    }
                }
                if(index!=-1)
                    this.onlineClients.RemoveAt(index);
            }
            Debug.WriteLine("current online clients:" + onlineClients.Count);
        }

        /// <summary>
        /// search local shared files and send the result to  remote client
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void SearchLocalFile(object sender, SearchEventArgs e)
        {
            Debug.WriteLine("Client: searching local shared files for:" + e.Key);

            List<CFileInfo> result = ClientUtils.SearchLibrary(this.files, e.Key, e.Type);
            CClientManager cm = (CClientManager)sender;
            cm.SendCommand(new CCResultCommand(result));
        }

        /// <summary>
        /// 按照给定的hash值搜索本地文件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public LocalFileInfo SearchSingleFile(object sender, RequesFileEventArgs e)
        {
            Debug.WriteLine("Client: searching file:" + e.FileHashCode);

            if (this.files == null || this.files.Count == 0)
                return null;

            var result = from f in files
                         where f.CFile.FileHashCode.Equals(e.FileHashCode)
                         select f;
            if (result.Count() != 0)
                return result.First();
            else
            {
                return null;
            }
        }
        #endregion  

        #region event
        /// <summary>
        /// occurs when client connected to remote server successfully
        /// </summary>
        public event ConnectingServerSucceedEventHandler ConnectingServerSucceed;

        protected  virtual void OnConnectingServerSucceed()
        {
            Debug.WriteLine("Connected to server successfully...");
            cServer = new ClientServer(info.IP,info.Port);
            cServer.SearchLocalFile+=new SearchEventHandler(this.SearchLocalFile);
            cServer.RequestFile+=new RequestFileEventHandler(this.SearchSingleFile);
            cServer.StartServer();
            //invoke the event handler here;
            if (this.ConnectingServerSucceed != null)
            {
                ConnectingServerSucceed(this,new ServerEventArgs(this.clientSocket));
            }
        }

        public event ConnectingServerFailedEventHandler ConnectingServerFailed;

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnConnectingServerFailed()
        {
            Debug.WriteLine("Connecting to server Failed...");
            //invoke the event handler here
            if (this.ConnectingServerFailed != null)
            {
                ConnectingServerFailed(this,new ServerEventArgs(this.clientSocket));
            }

            System.Threading.Thread.Sleep(10000);
            this.ConnectToServer();
        }
        #endregion

    }

}